Model Connection
This document describes the architecture and implementation of HAWKI's AI model connection system, including the data flow, components, and how to add new AI providers.
Table of Contents
- Architecture Overview
- Key Components
- Data Flow
- Provider Implementation
- How to Add a New Provider
- Streaming vs Non-Streaming Requests
- Error Handling
- Usage Analytics
Architecture Overview
HAWKI's AI integration uses a layered service architecture with dependency injection and factory patterns to process requests to various AI models (OpenAI, GWDG, Google, Ollama, OpenWebUI). The system provides a unified interface for interacting with different AI providers while handling model-specific requirements, streaming capabilities, and usage analytics.
Key Components
The AI connection system is built with a layered architecture consisting of the following components:
Service Layer
- AiService: Main entry point for AI interactions, providing unified methods for model retrieval and request processing
- AiFactory: Factory service responsible for creating provider instances, model contexts, and managing dependencies
- UsageAnalyzerService: Tracks and records token usage for analytics and billing purposes
Provider Layer
- ModelProviderInterface: Interface defining provider contract for model discovery and configuration
- ClientInterface: Interface defining client contract for request execution and model status checks
- AbstractClient: Base implementation providing common request validation and streaming fallback logic
Provider Implementations
- OpenAI: OpenAiClient, OpenAiRequestConverter, and specific request handlers
- GWDG: GwdgClient, GwdgRequestConverter, and specific request handlers
- Google: GoogleClient, GoogleRequestConverter, and specific request handlers
- Ollama: OllamaClient, OllamaRequestConverter, and specific request handlers
- OpenWebUI: OpenWebUiClient, OpenWebUiRequestConverter, and specific request handlers
Value Objects
- AiRequest: Immutable request object containing model reference and payload
- AiResponse: Response object with content, usage data, and completion status
- AiModel: Model definition with capabilities and context binding
- TokenUsage: Usage tracking data structure
Data Flow
Request Flow
- Entry Point: Client sends request to controller (e.g.,
StreamController
) - Service Resolution: Controller calls
AiService->sendRequest()
orsendStreamRequest()
- Request Processing:
AiService
resolves model and createsAiRequest
object - Model Resolution:
AiFactory
provides model instance with bound context - Client Delegation: Request is delegated to model's specific client (e.g.,
OpenAiClient
) - Request Conversion: Client uses converter to transform payload to provider format
- API Communication: Appropriate request handler executes HTTP call to provider API
- Response Processing: Raw response is converted to standardized
AiResponse
format - Usage Tracking: Token usage is extracted and recorded via
UsageAnalyzerService
- Response Delivery: Formatted response is returned to client
AiRequest Structure
class AiRequest
{
public ?AiModel $model = null;
public ?array $payload = null;
}
The payload array contains:
[
'model' => 'gpt-4o',
'stream' => true,
'messages' => [
[
'role' => 'user',
'content' => [
'text' => 'Hello, how are you?',
'attachments' => ['uuid1', 'uuid2'] // optional
]
]
],
'temperature' => 0.7, // optional
'top_p' => 1.0, // optional
// ... other provider-specific parameters
]
AiResponse Structure
class AiResponse
{
public array $content; // Response content with structured format
public ?TokenUsage $usage; // Token consumption data
public bool $isDone = true; // Completion status (false for streaming chunks)
public ?string $error = null; // Error message if any
}
Response content format:
[
'content' => [
'text' => 'AI-generated response text'
],
'usage' => [
'promptTokens' => 123,
'completionTokens' => 456,
'totalTokens' => 579
],
'isDone' => true
]
Provider Implementation
The new architecture separates concerns between model providers and clients, with dedicated request converters for payload transformation.
Core Interfaces
ModelProviderInterface - Defines provider contract:
interface ModelProviderInterface
{
public function getConfig(): ProviderConfig;
public function getModels(): AiModelCollection;
}
ClientInterface - Defines client contract:
interface ClientInterface
{
public function sendRequest(AiRequest $request): AiResponse;
public function sendStreamRequest(AiRequest $request, callable $onData): void;
public function getStatus(AiModel $model): ModelOnlineStatus;
}
Implementation Pattern
Each provider follows this structure:
- Provider Class (e.g.,
GenericModelProvider
): Handles model discovery and configuration - Client Class (e.g.,
OpenAiClient
): Manages request execution and delegation - Request Converter (e.g.,
OpenAiRequestConverter
): Transforms payloads to provider format - Request Handlers: Specific implementations for streaming/non-streaming requests
Example: OpenAI Implementation
class OpenAiClient extends AbstractClient
{
protected function executeRequest(AiRequest $request): AiResponse
{
return (new OpenAiNonStreamingRequest(
$this->converter->convertRequestToPayload($request)
))->execute($request->model);
}
protected function executeStreamingRequest(AiRequest $request, callable $onData): void
{
(new OpenAiStreamingRequest(
$this->converter->convertRequestToPayload($request),
$onData
))->execute($request->model);
}
}
Provider Examples
OpenAI Provider
class OpenAIProvider extends BaseAIModelProvider
{
public function formatPayload(array $rawPayload): array
{
// Transform payload to OpenAI format
}
public function formatResponse($response): array
{
// Extract content and usage from OpenAI response
}
// Other implemented methods...
}
Google Provider
class GoogleProvider extends BaseAIModelProvider
{
public function formatPayload(array $rawPayload): array
{
// Transform payload to Google Gemini format
}
public function formatResponse($response): array
{
// Extract content and usage from Google response
}
// Other implemented methods...
}
How to Add a New Provider
Adding a new AI provider requires implementing the provider pattern with separate components for model discovery, request handling, and payload conversion.
Implementation Steps
1. Create Provider Directory Structure
For a new provider (e.g., "MyProvider"), create the following structure:
app/Services/AI/Providers/MyProvider/
├── MyProviderClient.php
├── MyProviderRequestConverter.php
└── Request/
├── MyProviderNonStreamingRequest.php
├── MyProviderStreamingRequest.php
└── MyProviderUsageTrait.php
2. Implement the Client
<?php
namespace App\Services\AI\Providers\MyProvider;
use App\Services\AI\Providers\AbstractClient;
class MyProviderClient extends AbstractClient
{
public function __construct(
private readonly MyProviderRequestConverter $converter
) {}
protected function executeRequest(AiRequest $request): AiResponse
{
return (new MyProviderNonStreamingRequest(
$this->converter->convertRequestToPayload($request)
))->execute($request->model);
}
protected function executeStreamingRequest(AiRequest $request, callable $onData): void
{
(new MyProviderStreamingRequest(
$this->converter->convertRequestToPayload($request),
$onData
))->execute($request->model);
}
protected function resolveStatusList(AiModelStatusCollection $statusCollection): void
{
// Implement status checking for your provider's models
}
}
3. Create Request Converter
<?php
namespace App\Services\AI\Providers\MyProvider;
use App\Services\AI\Value\AiRequest;
class MyProviderRequestConverter
{
public function convertRequestToPayload(AiRequest $request): array
{
$rawPayload = $request->payload;
// Transform HAWKI format to your provider's expected format
return [
'model' => $rawPayload['model'],
'messages' => $this->formatMessages($rawPayload['messages']),
'stream' => $rawPayload['stream'] ?? false,
// Add other provider-specific parameters
];
}
private function formatMessages(array $messages): array
{
// Convert HAWKI message format to provider format
return array_map(function($message) {
return [
'role' => $message['role'],
'content' => $message['content']['text'] ?? ''
];
}, $messages);
}
}
4. Implement Request Handlers
<?php
namespace App\Services\AI\Providers\MyProvider\Request;
use App\Services\AI\Providers\AbstractRequest;
use App\Services\AI\Value\AiModel;
use App\Services\AI\Value\AiResponse;
class MyProviderNonStreamingRequest extends AbstractRequest
{
use MyProviderUsageTrait;
public function __construct(private array $payload) {}
public function execute(AiModel $model): AiResponse
{
return $this->executeNonStreamingRequest(
model: $model,
payload: $this->payload,
dataToResponse: fn(array $data) => new AiResponse(
content: ['text' => $data['choices'][0]['message']['content'] ?? ''],
usage: $this->extractUsage($model, $data)
)
);
}
}
5. Update Configuration
Add your new provider to the config/model_providers.php
file:
'providers' => [
'myprovider' => [
'active' => true,
'api_key' => env('MYPROVIDER_API_KEY'),
'api_url' => 'https://api.myprovider.com/v1/chat/completions',
'ping_url' => 'https://api.myprovider.com/v1/models',
'models' => [
[
'id' => 'my-model-1',
'label' => 'My Provider Model 1',
'streamable' => true,
'capabilities' => ['text', 'image']
]
]
]
]
6. Register with Dependency Container
The AiFactory
automatically discovers providers by convention. Ensure your provider class follows the naming pattern:
- Provider directory:
app/Services/AI/Providers/{ProviderName}/
- Client class:
{ProviderName}Client
- The factory will automatically instantiate and configure your provider when needed.
Key Implementation Notes
- Request Validation: The
AbstractClient
handles request validation automatically - Streaming Fallback: Non-streamable models automatically fall back to regular requests
- Usage Tracking: Implement the usage trait to extract token consumption data
- Error Handling: Use the base request class error handling patterns
- Model Capabilities: Define model capabilities (text, image, document processing) in configuration
4. Provider-Specific Considerations
When implementing a new provider, consider these aspects:
- API Format Differences: Understand how the API expects messages and returns responses
- Streaming Protocol: Implement the correct streaming protocol for the provider
- Usage Tracking: Extract token usage information correctly
- Error Handling: Handle provider-specific error responses
- Model Capabilities: Configure which models support streaming
5. Testing Your Provider
After implementing your provider, test it thoroughly:
- Test non-streaming requests
- Test streaming requests
- Verify error handling
- Check usage tracking
- Test with different message inputs
- Validate response formatting
Streaming vs Non-Streaming Requests
The AI service provides unified methods for both streaming and non-streaming requests with automatic fallback handling.
Non-Streaming Requests
Standard requests wait for the complete response:
// Using AiService
$response = $this->aiService->sendRequest([
'model' => 'gpt-4o',
'messages' => $messages
]);
// Returns complete AiResponse with content and usage
echo $response->content['text'];
Streaming Requests
Streaming requests deliver responses in real-time chunks:
// Using AiService with callback
$this->aiService->sendStreamRequest([
'model' => 'gpt-4o',
'stream' => true,
'messages' => $messages
], function(AiResponse $chunk) {
if (!$chunk->isDone) {
echo $chunk->content['text']; // Stream partial content
flush();
} else {
// Final chunk with usage data
$this->recordUsage($chunk->usage);
}
});
Automatic Streaming Fallback
If a model doesn't support streaming, the system automatically falls back to non-streaming mode:
// In AbstractClient
public function sendStreamRequest(AiRequest $request, callable $onData): void
{
if (!$request->model->isStreamable()) {
// Automatic fallback to non-streaming
$response = $this->sendRequest($request);
$onData($response);
return;
}
$this->executeStreamingRequest($request, $onData);
}
Error Handling
The system provides comprehensive error handling through multiple layers:
Exception Hierarchy
- AiServiceExceptionInterface: Base interface for all AI service exceptions
- ModelIdNotAvailableException: Thrown when requested model ID is not available
- NoModelSetInRequestException: Thrown when request lacks model specification
- IncorrectClientForRequestedModelException: Thrown when model/client mismatch occurs
Request Validation
// Automatic validation in AbstractClient
private function validateRequest(AiRequest $request): void
{
if ($request->model === null) {
throw new NoModelSetInRequestException();
}
// Validates client/model compatibility
if ($modelClient !== $this) {
throw new IncorrectClientForRequestedModelException(
$request->model->getClient(),
$this
);
}
}
Error Response Format
// Errors returned in AiResponse
$response = new AiResponse(
content: [],
error: 'Connection failed: timeout after 30s'
);
Usage Analytics
The UsageAnalyzerService
continues to track AI model usage, now working with the structured TokenUsage
value objects:
Token Usage Structure
class TokenUsage implements JsonSerializable
{
public function __construct(
public int $promptTokens,
public int $completionTokens,
public int $totalTokens
) {}
}
Usage Tracking
// Usage automatically extracted from responses
public function submitUsageRecord(TokenUsage $usage, string $type, string $model, ?string $roomId = null): void
{
UsageRecord::create([
'user_id' => Auth::id(),
'room_id' => $roomId,
'prompt_tokens' => $usage->promptTokens,
'completion_tokens' => $usage->completionTokens,
'total_tokens' => $usage->totalTokens,
'model' => $model,
'type' => $type,
]);
}
Integration with Responses
Usage is automatically tracked when processing AI responses:
// In request handlers, usage is extracted per provider
protected function extractUsage(AiModel $model, array $data): ?TokenUsage
{
if (!isset($data['usage'])) {
return null;
}
return new TokenUsage(
promptTokens: $data['usage']['prompt_tokens'] ?? 0,
completionTokens: $data['usage']['completion_tokens'] ?? 0,
totalTokens: $data['usage']['total_tokens'] ?? 0
);
}
Analytics Applications
This structured approach enables:
- Real-time Cost Tracking: Monitor token consumption across models
- Usage Pattern Analysis: Identify high-usage patterns and optimize
- Billing Integration: Accurate cost allocation per user/room
- Performance Monitoring: Track model efficiency and response times
- Resource Planning: Predict capacity needs based on usage trends